Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

[image_picker] Migrate iOS to Pigeon and improve state handling #5285

Merged
merged 10 commits into from
Apr 22, 2022

Conversation

stuartmorgan
Copy link
Contributor

Migrates the iOS implementation to a package-internal method channel based on Pigeon.

The driver for adopting pigeon here is that there are shared codepaths used for all three of the native method calls (across async callbacks, with state tracked in ivars), some of which return a single path (String) and some of which return multiple paths (List<String>). Currently making sure the right result type is passed entirely manual—which, as the fixed issue below demonstrates, is error prone. Pigeon allows us to have enforced type safety on the return type, so that it's impossible to accidentally call it the wrong way.

Incidental improvements made while updating the ObjC implementation:

  • Consolidated the state tracking to a single ivar which is a data class containing all relevant state.
  • Slightly reduced the reliance on the state ivar: it is now only set just before doing an async call; up until then the state is passed explicitly between methods. This makes the flow of the initial steps clearer, and helps avoid future cases like those we have had before where subtle ordering bugs in the initial synchronous method calls could cause incorrect behavior.
  • Consolidated all result calls to helpers: I noticed while adjusting some of the result calls to match the new callback signature that we had paths where we would return an error but not clear the state object. That would cause the next call to return a second error on the same result, which is a violation of the platform channel API. Now all errors, as well as all cancel paths, use a helper like the previously existing success case helper, which also clears state.

Not in scope:

  • Eliminating the use of an ivar to track state in first place (vs. using associated objects or similar to access it by controller from the async callbacks).

Part of flutter/flutter#94224
Fixes flutter/flutter#100132

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the relevant style guides and ran the auto-formatter. (Unlike the flutter/flutter repo, the flutter/plugins repo does use dart format.)
  • I signed the CLA.
  • The title of the PR starts with the name of the plugin surrounded by square brackets, e.g. [shared_preferences]
  • I listed at least one issue that this PR fixes in the description above.
  • I updated pubspec.yaml with an appropriate new version according to the pub versioning philosophy, or this PR is exempt from version changes.
  • I updated CHANGELOG.md to add a description of the change, following repository CHANGELOG style.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • All existing and new tests are passing.

@stuartmorgan
Copy link
Contributor Author

Some review notes:

  • @jmagman Feel free to delegate if there's someone who wants to start taking ownership of this iOS plugin implementation.
  • @gaaclarke I added you mostly for review of the Pigeon bits, but if you'd like to review more, the more eyes the better
  • CI will be totally broken for the moment because it references Pigeon 3.0.2, which doesn't exist yet. I'll re-run tests once that's reviewed and published. The published version of Pigeon makes invalid Dart from this file.
  • For anyone new to Pigeon: none of the files that have a comment at the top about being auto-generated by Pigeon need to be reviewed (unless you want to give feedback about Pigeon itself).
  • I deliberately changed the tests as little as possible so that the new implementation would be validated against the existing tests. Notes:
    • The one removed iOS test is because passing nil to the helper being tested there is now allowed (since cancel flows use it).
    • The other iOS changes are just to change the way the call is structured so that it's using the Pigeon API instead of raw serialization (which is a Pigeon implementation detail, so would be fragile to Pigeon updates).
    • While the Dart tests look new, they are actually a duplicate of the existing tests of the shared method channel implementation, changed as little as I could manage to make them work with Pigeon. The copy and then update are separate individual commits in the PR, so reviewing the test file changes commit that adopts Pigeon on the Dart side, rather than reviewing the entire test file, is probably more useful.

Copy link
Member

@gaaclarke gaaclarke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pigeon code looks good. I gave everything a look for clerical errors and didn't notice anything. This is definitely a big improvement. My one big nit is that the plugin is still storing a closure which is error prone and I mention in my comments above. It would be easier to work with and verify as correct if we were instead tying together callbacks and never storing the callback in an instance variable.

UIImagePickerController *imagePickerController = [self createImagePickerController];
imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext;
imagePickerController.delegate = self;
imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ];
self.callContext = context;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting this as an instance variable makes it hard to verify that the result is called in all cases. If all the branches to this method had callbacks for their asynchronous operations, we could see that they all correctly call the result back or not.

Copy link
Contributor Author

@stuartmorgan stuartmorgan Apr 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that part is still a mess. This is a direct replacement for the existing ivar state (reply, arguments), which had the same problem, just now consolidated to one object to make bookkeeping easier.

The reason I decided to explicitly make changing that not in scope (see PR description) is that I wanted to avoid turning the PR into a full rewrite. The pigeon changes are already substantial, but this keeps the actual logic flows mostly the same to limit the scope of what could be broken, and make the review more manageable.

We should absolutely fix that later.

- (void)handleSavedPathList:(NSArray *)pathList {
if (!self.result) {
- (void)returnSavedPathList:(nullable NSArray *)pathList {
if (!self.callContext) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we silently fail in these cases? This seems like a logical error if we get here, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably; this falls under the umbrella of leaving most of the code flows the same. I didn't want to risk introducing a new client-visible error here if there's a latent logic bug in the unchanged overall flow that this has been masking.

Fixing the use of ivars will eliminate the need for this method in the first place, so it'll get fixed then.

return path != null ? PickedFile(path) : null;
}

Future<String?> _getVideoPath({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: naming is a bit confusing since this says "getVideoPath" but is calling "pickVideo"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's some weird legacy naming in this plugin from the way things were named when the platform interface was created, and then when the new cross-file version were implemented. (This is just a copy from the shared method channel.)

I'll give it a better name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Standardized the helpers as _<host API name>AsPath. Still not great, but consistent, and it makes it more clear what the distinction is between the helper (returns paths) and the public variants (return XFile or PickedFile).

Comment on lines +20 to +24
bool operator ==(Object other) {
return other is _LoggedMethodCall &&
name == other.name &&
mapEquals(arguments, other.arguments);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tangent: Dart equality is kind of a mess. You never know if == means identity or value equality. I know there is prior art for mixing these up.

}
}

class _ApiLogger implements TestHostImagePickerApi {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you use a mocking library for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under this testing structure, a mocking library doesn't help; I'm already implementing all of the methods to log them anyway.

If I completely restructured the tests, then yes, but as mentioned in my initial PR comments I deliberately changed the tests as little as I could in this PR in order to ensure that implementations didn't regress something in a way that was masked by test change mistakes.

@stuartmorgan
Copy link
Contributor Author

Pigeon 3.0.2 has been published, so CI is now green.

@stuartmorgan stuartmorgan requested review from cyanglaz and removed request for jmagman April 20, 2022 17:16
@stuartmorgan
Copy link
Contributor Author

Switching iOS owners review to @cyanglaz per new code owner assignments.

*/
- (void)handleSavedPathList:(NSArray *)pathList {
if (!self.result) {
- (void)returnSavedPathList:(nullable NSArray *)pathList {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nits: The method name suggests it should return the list. Is there a better way to rename it so that it is clearer that this method is sending the result to method channel. (I can't think of any though)
If no better name is found, maybe update the doc so it is more explicit:

"then returns it in callContext.result of the original method channel call"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed both of the response methods to sendCallResultWith*, and also updated the comments per your suggestion, so they should be much more explicit about what they are doing.

(Long term, the real solution is to get rid of these methods.)

Copy link
Contributor

@cyanglaz cyanglaz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM % nits

@stuartmorgan stuartmorgan added the waiting for tree to go green (Use "autosubmit") This PR is approved and tested, but waiting for the tree to be green to land. label Apr 22, 2022
@fluttergithubbot fluttergithubbot merged commit 10c8f9e into flutter:main Apr 22, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Apr 25, 2022
mauricioluz pushed a commit to mauricioluz/plugins that referenced this pull request Jan 26, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
p: image_picker platform-ios waiting for tree to go green (Use "autosubmit") This PR is approved and tested, but waiting for the tree to be green to land.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[image_picker][iOS]: pickMultiImage throws an invalid type cast exception on iOS 12
5 participants